Skip to content

v7: @decocms/start → 14-package lockstep monorepo (blocks core, admin, CLI, Next.js + TanStack bindings, per-platform apps)#315

Open
tlgimenes wants to merge 85 commits into
mainfrom
v7
Open

v7: @decocms/start → 14-package lockstep monorepo (blocks core, admin, CLI, Next.js + TanStack bindings, per-platform apps)#315
tlgimenes wants to merge 85 commits into
mainfrom
v7

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

The v7 line: @decocms/start (single legacy package) split into a bun-workspace monorepo of 14 lockstep-versioned packages, releasing independently from this branch via semantic-release (blocks-v* tags, npm OIDC trusted publishing). Published through 7.5.3; verified end to end against four production storefronts.

main keeps shipping legacy @decocms/start 6.x until this line becomes primary.

Package map

Layer Packages
Framework core @decocms/blocks — CMS block resolution, section registry, matchers, portable SDK (framework-agnostic)
Admin protocol @decocms/blocks-admin — Studio handlers: /live/_meta, /.decofile, invoke, render/previews
Codegen & tooling @decocms/blocks-cli — generators (blocks/sections/loaders/schema/invoke), site migration scaffolder
Platform bindings @decocms/tanstack (Vite plugin, worker entry, daemon), @decocms/nextjs (App Router glue)
Commerce apps @decocms/apps-{vtex,shopify,magento,salesforce,algolia,commerce,website,blog,resend} — one package per platform, replacing the @decocms/apps monolith

Lockstep versioning: one change releases all 14 packages at the same version — no cross-package matrix.

Highlights

Next.js glue tier (new)

  • createNextSetup() — one-call site bootstrap (decofile loading, section/matcher registration, admin schema, memoized with rejection-retry)
  • createDecoRouteHandlers() — the entire Studio protocol from ONE catch-all route (app/deco/[[...deco]]/route.ts), accepting both pre-rewrite (/.decofile, /live/_meta, /live/previews/*) and /deco/* URL forms, with method gating (invoke POST-only — closes a GET/CSRF vector; meta GET-only)
  • withDeco() next.config wrapper (CJS-requireable) — Studio-URL rewrites + transpilePackages
  • Full recipe in packages/nextjs/README.md; examples/nextjs-smoke consumes all three

.deco/ is the framework's folder

  • Generators default their outputs into .deco/ (blocks.gen.*, loaders.gen.ts, sections.gen.ts, meta.gen.json) with a legacy-artifact warning at old paths; generate-sections --registry emits a lazy import map (the non-Vite equivalent of import.meta.glob)
  • invoke.gen.ts deliberately stays in src/ — empirically proven: TanStack Start's server-fn resolution 500s when the compiled split module lives outside the app source tree (documented in the generator header with the repro)

Correctness & portability

  • react-server safety: admin route handlers no longer crash on createContext (root barrels split; lazy contexts; "use client" boundaries) — route handlers ignore directives and run on React's react-server build
  • createServerFn compiler crashes under real npm installs fixed (createInvokeFn rewritten as a codegen-only marker; optimizeDeps.exclude)
  • Zero import.meta syntax in shipped source — CJS consumers (ts-jest) can compile the raw-TS packages
  • Schema definition IDs are repo-relative (stable across machines/CI); codegen skips *.test/spec/stories/gen.* files
  • tsx floored at ^4.22.5 (4.22.0–4.22.4 loader-hook bug returned empty namespaces from tsImport inside Vite dev)

Migration scaffolder overhauled

~230 emitted imports updated from pre-split @decocms/start/@decocms/apps paths to the 7.x package set (detect-patterns for legacy sites deliberately untouched); fixed a verify gate that would have failed 100% of migrations; Shopify hooks mapping repaired.

Ops

  • Release workflow: npm pinned to 11.x (npm 12.0.0 shipped a broken sigstore/provenance path); OIDC per-package trusted publishing

Verified against production

Four real storefronts migrated and verified on this line (bagaggio, casaevideo, lebiscuit on TanStack/Workers; FILA FastStore on Next.js): production-vs-preview parity diffs show byte-identical /.decofile and structurally identical /live/_meta schemas (modulo the deliberate ID renaming), full admin protocol serving, builds green, live deploy previews on every PR.

Merge note

main and v7 are structurally incompatible (flat src/ vs packages/*) — a normal merge cannot resolve this PR. When v7 becomes primary: squash-merge or branch-swap, decided deliberately.

…o + ship @decocms/next

Squashed history of PR #307 (originally merged to a branch named 7.x,
then migrated to v7 — see below). Three phases plus release
automation:

Phase 1 — Monorepo split
=========================
Splits the single `@decocms/start` package into a 5-package Bun
workspace with a one-way dependency graph:
  packages/live     → @decocms/live     (CMS core: cms/, matchers/,
                       sdk/, hooks/, types/, admin schema composition —
                       named "live" not "runtime": @decocms/runtime
                       already exists on npm as a real, unrelated,
                       actively-published package at v2.3.1)
  packages/admin    → @decocms/admin    (admin protocol handlers,
                       depends on live)
  packages/cli      → @decocms/cli      (codegen scripts + Fresh→
                       TanStack migration tooling, depends on live)
  packages/tanstack → @decocms/tanstack (TanStack Start + Cloudflare
                       Workers binding: workerEntry, cmsRoute, vite
                       plugin, daemon; depends on live + admin)
  packages/next     → @decocms/next     (new)

Root-caused and fixed the real bug motivating the split: dist/tsup
bundling caused module-state duplication across package boundaries,
which is why an earlier attempt at this shipped as v5.2.2 and had to
be reverted. This split ships plain .ts source exports with no
bundling step.

Phase 2 — @decocms/next (Next.js App Router binding)
======================================================
New package mirroring @decocms/tanstack's capabilities for Next.js:
SectionRenderer, DeferredSectionBoundary (RSC Suspense streaming),
client-only-forced section rendering, DecoPageRenderer,
createDecoPage, DecoRootLayout, and Route Handler re-exports of the
admin protocol. Validated against tanstack-smoke and next-smoke
fixtures.

Follow-up cleanup
==================
Refreshed README.md, CLAUDE.md, and deep-dive docs for the new
architecture. Replaced GAP_ANALYSIS.md + GAP_ANALYSIS_V2.md with
docs/known-gaps.md. Audited all ~30 .cursor/skills/ directories:
deleted 8 stale ones, merged 2 near-duplicate pairs, path-fixed the
rest. Forward-ported 3 fixes that landed on main while this branch
was in flight (x-deco-matchers-override, @title/@ignore JSDoc on app
loaders, a vite dev-watcher delta-apply fix for a real production
incident).

Release automation (v7 branch, ci/release.yml + .releaserc.v7.json)
======================================================================
main keeps releasing the legacy @decocms/start (v*.*.* tags,
unchanged config). v7 releases the 5 new @decocms/* packages
independently, under a separate tag namespace (live-v*.*.*) via a
dedicated semantic-release config — deliberately NOT sharing main's
`branches` array, since main's @decocms/start and the 5 new packages
are unrelated npm packages with no real version relationship.

Two hard-won, non-obvious fixes baked into this config, both costly
to rediscover:

1. The branch cannot be named like a semver range ("7.x"). Semantic-
   release auto-classifies any `N.x`-shaped branch NAME as a
   "maintenance" branch — regardless of how it's written in `branches`
   config, since classification runs on the resolved branch name, not
   the config-side glob. Maintenance branches are structurally
   required to have a LOWER version than a companion "release"-type
   branch, which is backwards from what we want (v7 should be newer
   than legacy main). No in-branches-array trick escapes this
   (confirmed empirically against real semantic-release source:
   lib/branches/index.js's per-type branchesValidator). Renamed the
   branch 7.x -> v7 to sidestep it entirely — "v7" doesn't match the
   range-shape pattern, so it's just a normal branch with a seeded
   starting tag (live-v7.0.0, on a commit unique to this branch's
   history, NOT shared with main — sharing it caused semantic-release
   to compute an impossible `>=7.0.0 <7.0.0`-shaped range once, since
   both branches would "see" the same tag as their own last release).
   v7 still needs a companion branch in `branches` (semantic-release
   requires ≥1 non-maintenance-shaped branch even for a single-branch
   config) — used a genuine orphan placeholder branch
   (decocms-live-legacy-anchor, pinned at a fixed v0.0.1 seed tag)
   rather than main, since main's own fluctuating computed version
   was ALSO capping v7's valid range in confusing ways.

2. semantic-release's `--dry-run` only skips its OWN native git-tag
   creation step. It calls `plugins.prepare()` and `plugins.publish()`
   UNCONDITIONALLY either way, and @semantic-release/exec's
   prepare/publish hooks don't check dry-run themselves. A naive
   exec-based publishCmd would attempt a REAL `npm publish` during a
   "dry run". .releaserc.v7.json's publishCmd checks a `$DRY_RUN` env
   var (set by the workflow from the workflow_dispatch dry_run input)
   before calling npm — verified this is a real, not theoretical, risk:
   an early version of this config without the guard reached a real
   `npm publish --access public` for @decocms/admin during testing
   (caught immediately; failed on 404 only because publish permissions
   aren't provisioned yet, not because of the dry-run guard — that
   guard didn't exist at the time).

Added a workflow_dispatch trigger with a dry_run input (default true)
specifically so the version computation and publish plan can be
inspected in real CI logs before trusting an automatic push-triggered
release — this is genuinely a one-way door (5 packages that have never
been published before) and deserves that scrutiny. First verified
computation: 7.0.1.

Known follow-up, not yet resolved: whatever npm credentials are
currently configured for this repo's Actions (NODE_AUTH_TOKEN via
actions/setup-node) return 404 (not authorized to CREATE new packages)
for @decocms/admin and @decocms/cli — confirmed via the real publish
attempt above. The actual first release will need proper npm token
provisioning with create-package rights on the @decocms org before it
can succeed.

Verified: all 5 packages typecheck clean and pass their full test
suites via vitest (live 455, admin 15, next 10, cli 369, tanstack 59
— 908 tests total).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tlgimenes tlgimenes requested a review from a team July 7, 2026 18:42
@tlgimenes tlgimenes requested a review from vibe-dex as a code owner July 7, 2026 18:42
tlgimenes and others added 8 commits July 7, 2026 15:52
…decision)

Same rationale/mechanics as the earlier @decocms/runtime -> @decocms/live
rename: renamed packages/live/ -> packages/blocks/ (git mv, preserving
file history) and every @decocms/live import + bare packages/live path
reference (102 files) to @decocms/blocks.

Also renamed the release-line naming to match: tagFormat
live-v${version} -> blocks-v${version} in .releaserc.v7.json, and
the placeholder companion branch decocms-live-legacy-anchor ->
decocms-blocks-legacy-anchor (new orphan branch + blocks-v0.0.1 seed
tag pushed; old decocms-live-legacy-anchor branch and live-v* tags
left in place for now, not yet deleted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…decocms/cli -> @decocms/blocks-cli

Disambiguates both packages as part of the @decocms/blocks family —
'admin' and 'cli' alone are generic enough to be confusing on their
own, unlike @decocms/tanstack and @decocms/next which are already
self-explanatory as framework bindings (left unchanged).

Renamed packages/admin/ -> packages/blocks-admin/ and packages/cli/ ->
packages/blocks-cli/ (git mv, preserving file history), plus every
@decocms/admin and @decocms/cli import + bare packages/admin and
packages/cli path reference (65 files total) to the new names.
Cross-package workspace dependencies (packages/next and
packages/tanstack both depend on @decocms/admin;
packages/tanstack also depends on @decocms/cli) updated in the same
pass. bin script entries in blocks-cli/package.json use relative
paths, unaffected by the rename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…in/blocks-cli siblings

vitest run --root ../.. packages/blocks was matching as a path PREFIX,
not an exact directory — since packages/blocks-admin and
packages/blocks-cli both start with the substring 'packages/blocks',
running @decocms/blocks's own test script incorrectly pulled in both
siblings' test files too (35 -> 58 files, 455 -> 839 tests). A
trailing slash forces an exact directory boundary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e provisioned

3+ push-triggered runs during development created real git tags
before failing on npm 404 (CI's NODE_AUTH_TOKEN lacks create-package
rights on @decocms org), each requiring manual tag cleanup. Not
removing the capability, just the automatic push trigger — re-add
'v7' to the push branches list once a proper npm Automation token is
configured, then use workflow_dispatch for the actual first release
rather than relying on the next push to catch it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…citness

Renamed packages/next/ -> packages/nextjs/ and examples/next-smoke/ ->
examples/nextjs-smoke/ (git mv, preserving file history), plus every
@decocms/next import + bare packages/next and examples/next-smoke path
reference to the new names.

Also proactively added trailing slashes to every package's
'vitest run --root ../.. packages/X' test script (not just the ones
with a live collision) — packages/blocks's own test script silently
picked up packages/blocks-admin and packages/blocks-cli's tests too in
the previous rename (path-prefix match, not exact directory), and nextjs'
name is now a substring-prefix risk for any future packages/nextjs-*
addition. Trailing slash forces an exact directory boundary in all cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NODE_AUTH_TOKEN is now a granular access token with 2FA bypass and
read/write access to the decocms org + all packages. All 5 v7
packages were bootstrapped to 7.0.0 via a manual local publish
(confirmed live on npm) using the same token; future commits release
through this trigger normally.

Also added .npmrc to .gitignore (holds npm auth tokens during local
publish sessions) and fixed a stale examples/next-smoke comment
missed in the nextjs rename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red token

Removes the NODE_AUTH_TOKEN dependency entirely — each of the 5 v7
packages now has this repo's release.yml configured as its trusted
publisher on npmjs.com. Adds an explicit npm upgrade step since
Trusted Publishing needs npm >=11.5.1 and Node 22's bundled npm is
older. Also deleted the now-unused NODE_AUTH_TOKEN repo secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ication

npm Trusted Publishing (OIDC) auto-generates sigstore provenance
attestations and cross-checks package.json's repository.url against
the actual GitHub repo the OIDC token came from — publish fails with
E422 if repository is missing. Adds repository.url +
directory to all 5 v7 packages, pointing at
https://github.com/decocms/blocks (this repo's current name).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.0.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

The declared ^1.28.0 floor is too loose: otel.ts uses
ATTR_DEPLOYMENT_ENVIRONMENT_NAME, which doesn't exist until 1.41.1
(verified directly against package tarballs — absent in 1.40.0's
stable_attributes.js, present in 1.41.1's). deco-start's own tree
happens to resolve a high-enough version so this never surfaced
locally, but a consumer (faststore-fila) with a different dependency
tree resolved 1.40.0 and hit a hard TS2724 type error on this package's
own otel code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.0.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

…mports

The full @decocms/blocks/cms barrel (index.ts) re-exports loader.ts and
resolve.ts, which transitively import node:async_hooks and
node:fs/promises. Bundling ANY export from that barrel for a browser
target — even one with zero Node dependencies itself, like
getResolvedComponent — drags the whole module graph in, since ES
imports are evaluated per-file, not per-export. Turbopack rejects this
outright in production builds; webpack has historically let it
through uncaught. Found via faststore-fila's migration to this
package: a Client Component importing only getResolvedComponent
failed Turbopack's production build.

New @decocms/blocks/cms/client entry point exports just the
verified-safe subset: registry.ts (component lookups — imports only
React types), sectionMixins.ts (goes through useDevice.ts ->
requestContext.ts, whose only node:async_hooks dependency is already
swapped for a browser stub via this package's existing "browser"
export condition), and schema.ts (zero imports). Deliberately excludes
loader.ts, resolve.ts, sectionLoaders.ts, loadDecofileDirectory.ts,
blockSource.ts, and applySectionConventions.ts — resolver/storage
concerns that only make sense server-side.

Verified with a real esbuild browser-target bundle, not just tsc:
client.ts bundles clean with zero node: references; index.ts genuinely
fails the same bundle with real node:async_hooks resolution errors,
proving both that the original bug is real and that the split fixes
it. Kept as an automated regression test
(client.browserBundle.test.ts) — shells out to the esbuild CLI binary
rather than using its JS API, since the JS API's internal
TextEncoder/Uint8Array invariant check fails across Vitest's
module-isolation VM boundary (confirmed the invariant holds under
plain `bun -e`; it's a realm mismatch, not a real environment problem).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

…arrel split

DecoPageRenderer.tsx (packages/tanstack/src/hooks/) is client-bundled
(useState/useEffect/Suspense/lazy) and only used runtime imports that
are all in the new cms/client subset (getResolvedComponent,
getSectionOptions, getSectionRegistry, getSyncComponent,
preloadSectionModule, setResolvedComponent, SectionOptions) — a real
consumer of the split published as @decocms/blocks@7.1.0. Its
DeferredSection/ResolvedSection type-only imports stay on the full
@decocms/blocks/cms barrel, since type imports are erased at compile
time regardless of source path, and neither type is in cms/client
anyway.

Swept the docs/skills that describe this import path for staleness:
- CLAUDE.md: added a cms/client row to the package-exports table with
  guidance on when to use it vs the full barrel; also fixed the
  Package column still saying "runtime"/"next" from before the
  @decocms/runtime->live->blocks and @decocms/next->nextjs renames.
- import-mapping.md (deco-next-package-migration skill): getResolvedComponent's
  row still described the dev-diagnostic page's view component as "a
  client component" — no longer true, it was converted to a Server
  Component in faststore-fila specifically because of this bug. Added
  the concrete fix-by-case breakdown and pointed future readers at
  cms/client for the common case.
- docs/hydration-and-ssr-migration.md: the DecoPageRenderer
  syncThenable fix sketch didn't show an import statement; added a
  note that getResolvedComponent there needs cms/client, not the full
  barrel, since it's the exact component this session found and fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.1.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

…ability.ts

sdk/observability.ts re-exports instrumentWorker from ./otel, but
otel.ts imported configureMeter/configureTracer/getActiveSpan back
from ./observability — a real cycle (observability.ts -> otel.ts ->
observability.ts). Confirmed via a real Rollup warning surfaced while
building casaevideo-tanstack: "will likely lead to broken execution
order" when the two modules land in different chunks.

Fix: otel.ts now imports those three functions directly from
../middleware/observability (where they're actually defined —
sdk/observability.ts just re-exports them), the same module it
already imported METRIC_METADATA from. Breaks the cycle without
changing any public API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.1.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

tlgimenes and others added 9 commits July 7, 2026 18:38
Captures the 2026-07-07 design session: split-by-platform package
taxonomy (blocks-* for framework, apps-* for integrations,
nextjs/tanstack for framework bindings), the matcher/flags/UI-component
duplication found and how to resolve it, lockstep release integration,
and what happens to the old apps-start repo. Design only — no
implementation yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed with the user: blocks/hooks (existing subpath, already houses
similar UI primitives like RenderSection/LazySection) over a new
blocks/commerce subpath, to minimize new export-surface additions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onorepo

15 tasks: shared migration tooling, apps-commerce (foundational), UI
component relocation to blocks/hooks, matcher/flags cleanup in blocks,
apps-website (reduced scope), then apps-vtex/shopify/magento/algolia/
salesforce/resend/blog, end-to-end verification against faststore-fila,
and a deprecation notice on the old apps-start repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…admin/apps/autoconfig mapping

Verified in Task 1 Step 2 by comparing apps-start's registry.ts doc comment
(references autoconfigApps()) against packages/blocks-admin/src/apps/autoconfig.ts,
which exports an equivalent autoconfigApps(blocks, registry) and documents the
old call site directly. Matches the mapping already in this repo's CLAUDE.md.
Moves apps-start's commerce/ (types, app-types, resolve, manifest-utils,
utils/*, sdk/*) and root registry.ts into a new @decocms/apps-commerce
package, excluding commerce/components/ (Task 3 moves those UI components
elsewhere). Rewrites @decocms/start/* imports via
scripts/migrate-apps-import.mjs; the registry.ts doc comment's remaining
@decocms/start references are updated by hand to the confirmed
@decocms/blocks-admin/apps/autoconfig mapping.

Test files that lived in __tests__/ subdirs in apps-start are flattened to
sibling *.test.ts files to match this repo's co-located-test convention;
their relative imports were fixed accordingly (../foo -> ./foo).

Known gap, not resolved here: registry.ts's APP_REGISTRY entries dynamically
import platform mod modules (./shopify/mod, ./vtex/mod, ./resend/mod,
./blog/mod) that lived alongside commerce/ in apps-start's single package.
Now that each platform is a separate @decocms/apps-<platform> package
depending one-way on @decocms/apps-commerce, resolving those imports would
require the reverse edge (apps-commerce -> apps-shopify -> apps-commerce),
violating the monorepo's one-way dependency rule. Left as @ts-expect-error
stopgaps with an explanatory comment pending a plan-level decision (e.g. a
site-level registry aggregator, or non-statically-typed string entries)
rather than guessing a fix.

Verified: bun install clean; packages/apps-commerce typecheck + test clean
(5 files, 50 tests); whole-workspace typecheck and test clean across all 6
packages (blocks, apps-commerce, blocks-admin, blocks-cli, nextjs, tanstack).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…m entries

Task 2's implementation surfaced a real conflict: apps-start's registry.ts
was a single static array whose entries relatively-imported every
platform's mod.ts, only possible because everything lived in one package.
Split by platform, no apps-* package can hold that array without
depending on every other apps-* package, violating the one-way rule.

User decision: apps-commerce/registry keeps only the shared types; each
platform package with a registrable app (vtex, shopify, resend, blog)
exports its own single-entry registry from its own package, and sites
compose their own array explicitly. Updates Tasks 7/8/12/13's scope
accordingly (documented here; each task's own section gets the concrete
addition when reached).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stry types

Resolves the circular-dependency gap flagged in the previous commit: registry.ts
no longer holds a static APP_REGISTRY array whose entries dynamically imported
sibling platform mods (./shopify/mod, ./vtex/mod, ./resend/mod, ./blog/mod) that
can't resolve from this package without creating a reverse apps-commerce ->
apps-<platform> -> apps-commerce dependency edge.

Per decision: each platform package with a registrable app now exports its own
single-entry registry from its own ./registry subpath (Tasks 7, 8, 12, 13 — not
this task). apps-commerce/registry.ts keeps only the shared AppRegistryEntry /
AppRegistry types those per-platform registries will import. The
@decocms/apps-commerce package.json's "./registry" export subpath is unchanged.

Verified: packages/apps-commerce typecheck + test clean (5 files, 50 tests);
whole-workspace typecheck clean across all 6 packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t commerce/components)

Moves Image, Picture, and the JsonLd SEO components from apps-start's
commerce/components/ into @decocms/blocks/hooks (existing subpath, per
the design's resolved decision to reuse it rather than add
blocks/commerce). Image and Picture were already self-contained (no
commerce-type imports) and moved unchanged; ported Image's Vitest
suite alongside it.

JsonLd.tsx imported Product/ProductListingPage/BreadcrumbList/etc. from
apps-start's commerce types module (now @decocms/apps-commerce/types),
which @decocms/blocks cannot depend on (one-way rule: apps-* depends on
blocks, never the reverse). Each JsonLd function only reads a small,
flat subset of those schema.org types (name/url/sku/image[].url/
offers.price/aggregateRating.ratingValue, etc.) — the original code
already didn't fully trust the nominal Product.offers type either (it
cast to `Offer[] | AggregateOffer`). Replaced the import with local,
minimal structural types (JsonLdProduct, JsonLdProductListingPage,
JsonLdBreadcrumbList) that describe just what's read; any real
apps-commerce Product/ProductListingPage/BreadcrumbList value is a
structural superset and can be passed in without a cast. No `JsonLd`
symbol exists in the source file (only ProductJsonLd, PLPJsonLd,
BreadcrumbJsonLd, seoMetaTags), and Picture.tsx has no default export
(named Picture/Source) — the barrel exports the components' actual
names rather than the brief's placeholder `default as X` shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tlgimenes and others added 9 commits July 8, 2026 18:21
Composes createSiteSetup, applySectionConventions, loadBlocks (all
route-handler-safe) with loadDecofileDirectory for filesystem decofile
loading, and lazily imports @decocms/blocks-admin for meta/renderShell/
previewWrapper so the eager import graph never touches module-scope
client-React. Returns a memoized ensureSetup() for Task 6's dispatcher
and fila's Task 10 to call before every admin/route-handler request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
setupPromise ??= (...) permanently cached a rejected first attempt,
so one transient failure (fs blip during loadDecofileDirectory, flaky
meta() fetch) would poison the warm serverless instance forever. Now
the memo is cleared on rejection so the next ensureSetup() call
retries, while the triggering call still rejects with the original
error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds createDecoRouteHandlers() to routeHandlers.ts: a single GET/POST
dispatcher for app/deco/[[...deco]]/route.ts that routes /deco/decofile,
/deco/meta, /deco/render, /deco/invoke/*, and /deco/previews/* (rebuilt to
the /live/previews/* prefix handleRender parses) to the existing
blocks-admin handlers, running an optional setup() before each request and
404-ing unknown paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oke branch

The catch-all dispatcher wired the same handler to GET and POST with no
method check on invoke/*. handleInvoke has no auth and falls back to a
?props=<json> query string for GET, so an unauthenticated <img> tag on a
third-party page could trigger mutating VTEX actions cross-site (GET is a
CORS simple request, no preflight). The per-URL invokePOST export this
dispatcher replaces was POST-only for exactly this reason — restore it,
and 405 non-GET on meta (read-only schema endpoint) for symmetry.

Also pins the previews-rebuild's `new Request(rebuilt, request)` body-
forwarding semantics with a test, and documents why a plain-object init
would throw "duplex option is required" on a future refactor. That test
needs Node's native Request (jsdom's fetch polyfill drops the body on
Request-as-init), so routeHandlers.test.ts now runs under
@vitest-environment node, matching this package's setup.test.ts precedent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lePackages)

Adds packages/nextjs/src/config.cjs (CJS, requireable from a CJS
next.config.js despite the package being "type": "module") exporting
withDeco() and DECO_REWRITES, plus a src/config.d.cts declaration and
a "./config" exports map entry. withDeco() prepends the three Studio
rewrites (/.decofile, /live/_meta, /live/previews/:path*) that Task 6's
dispatcher expects, ahead of any user rewrites in array or object form,
and dedupes transpilePackages with the @decocms packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s/createNextSetup + README recipe

Migrates examples/nextjs-smoke off its ad-hoc rewrites/route files onto the
three new @decocms/nextjs surfaces built in Tasks 5-7 (withDeco,
createDecoRouteHandlers, createNextSetup), and adds packages/nextjs/README.md
as the copy-paste recipe a new Next.js site follows.

While validating end-to-end against a real `next build`/`next dev`, found and
fixed two real bugs surfaced by the migration (not just fixture wiring):

- routeHandlers.ts's dispatch derived the requested action solely from a
  `/deco/`-prefixed pathname, on the assumption that a next.config rewrite
  hands route handlers the destination path. Verified empirically (dev and
  `next start`) that Next.js instead hands `request.url`/`nextUrl.pathname`
  as the ORIGINAL, pre-rewrite path — so every rewritten protocol URL
  (`/.decofile`, `/live/_meta`, `/live/previews/*`) 404'd. `resolveAction`
  now accepts both forms; added regression tests hitting the rewrite-source
  paths directly (the prior tests only ever constructed already-/deco/*
  Requests, so they'd have kept passing against the broken dispatcher).
- Picture.tsx (reachable from DecoRootLayout/SectionRenderer via the
  hooks/index.ts barrel) imports createContext/useContext without
  "use client", which crashes Next's react-server module-graph check for any
  page that imports the root barrel. Added "use client", same precedent as
  SectionErrorFallback.tsx's existing fix for the same class of issue.

Also dropped a stray console.log left in blocks-admin's handleRender.

Verification: `examples/nextjs-smoke` builds clean; dev server curls of
/.decofile, /live/_meta, and / (CMS-resolved Hero content) all 200; repo-wide
`bun run typecheck && bun run test` green across all packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…he block comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…enance path

npm 12.0.0 (published 2026-07-08T21:06Z, ~1h before our release run)
fails every 'npm publish' with "Cannot find module 'sigstore'" from
libnpmpublish's provenance.js. Two release runs died on it; the
morning's 7.3.1 release on the same runner image succeeded because
@latest still resolved to 11.18.0 then. Pin the major; revisit after
npm 12 stabilizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

tlgimenes and others added 3 commits July 8, 2026 21:01
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tifacts live in the framework's folder)

Flips the default output path of four blocks-cli generators from
src/server/{cms,admin}/ to .deco/, so generated framework artifacts
live alongside .deco/blocks instead of scattered under app source:

  - generate-blocks   -> .deco/blocks.gen.ts (+ sibling .deco/blocks.gen.json,
                          derived from --out-file by extension swap)
  - generate-loaders  -> .deco/loaders.gen.ts
  - generate-sections -> .deco/sections.gen.ts
  - generate-schema   -> .deco/meta.gen.json

generate-invoke is the one exception and keeps its default at
src/server/invoke.gen.ts: unlike the other four (inert data/metadata
snapshots the framework loads at runtime), it emits actual app
server-function code that TanStack Start's build-time compiler
transforms (createServerFn().handler() -> RPC stubs), so it belongs
in src/ with the rest of the compiled app code. Documented inline.

Legacy guard: when no explicit --out/--out-file flag is passed AND
the OLD default file still exists on disk, each generator prints a
one-line stderr warning (lib/legacyArtifact.ts) naming both paths and
telling the caller to move the file and update its importers, then
writes to the NEW default anyway. An explicit flag skips the guard
entirely (deliberate choice, no warning). Every flipped generator
mkdir -p's the new output directory before writing.

Updated in the same commit (ships in the same lockstep release):
  - packages/tanstack/src/vite/plugin.js dev-mode watcher, which called
    generateBlocks()/generate-schema.ts relying on the old defaults
  - packages/blocks-cli/scripts/migrate.ts's scaffolding templates
    (setup.ts, commerce-loaders.ts, knip-config.ts) that imported from
    or referenced the old default paths
  - packages/nextjs README + setup.ts JSDoc: scripts no longer need
    --out/--out-file, and the recipe now imports generated artifacts
    through a `deco/*` tsconfig path alias -> .deco/* instead of a
    relative import, since src/deco/setup.ts isn't adjacent to the
    site-root .deco/ directory
  - packages/blocks-admin's "unknown handler" error message and
    packages/blocks/src/setup.ts JSDoc, which pointed at the old path

examples/nextjs-smoke and examples/tanstack-smoke construct their
setup inline and don't invoke any of these generators, so they needed
no changes.

Tests: extended generate-sections.test.ts and generate-schema.test.ts
with subprocess fixtures covering the new default path, the legacy
warning (both paths + guidance text on stderr, new default still
written), and the explicit-flag no-warning case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
generate-blocks/loaders/sections/schema now default their output to
.deco/ instead of src/server/{cms,admin}/ (commit 36ebf2a). Update the
two migration-skill docs that hardcoded the old paths in troubleshooting
commands and the setup.ts template's generated-artifact imports.
generate-invoke's src/server/invoke.gen.ts default is unchanged and was
left as-is everywhere it's referenced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.5.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

tlgimenes and others added 2 commits July 8, 2026 21:24
The spawnSync-tsx fixture tests take ~6s under full-suite parallel load,
tripping vitest's 5s default — 2 flaky failures on whole-monorepo runs
that never reproduced in isolated package runs. test: type on purpose —
no release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… inside Vite

Root cause of the long-standing non-fatal "[deco] blocks bootstrap
failed: TypeError: generateBlocks is not a function" cold-start warning
on lebiscuit-tanstack: tsx 4.22.0-4.22.4 has a loader-hook state bug
(fixed upstream in 4.22.5, "isolate hook state per async
module.register() registration"). Inside a Vite dev-server process,
tsImport(@decocms/blocks-cli/generate-blocks) resolves the module
correctly but returns an EMPTY namespace — no rejection, which is why
every out-of-band reproduction (plain node, same parent URL, same tsx
copy) passed while the in-process call failed. Sites whose lockfiles
froze 4.21.0 (baggagio, casaevideo, fila) never saw it; lebiscuit's
lockfile froze 4.22.0.

Verified in both directions on lebiscuit: tsx 4.21.0 -> bootstrap
succeeds; 4.22.0 -> empty namespace; 4.23.0 -> succeeds.

blocks-cli's tsx range was ^4.19.0, so any fresh install could land in
the broken window — floor it at ^4.22.5. The tanstack vite plugin's
loadGenModule also now fails with an actionable message (naming the tsx
window and the upgrade command) when it sees an empty namespace,
instead of the bare "generateBlocks is not a function" that a site's
own hoisted-broken-tsx lockfile pin would still surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.5.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

tlgimenes and others added 3 commits July 8, 2026 22:09
…src/

Smoke-tested moving it to .deco/ on a real site: TanStack Start's client
stub transform handles the new path fine, but the server half can't
resolve the split module back to an executable handler — every
/_serverFn call 500s (unhandled, no stack) while the storefront and
typecheck stay green. Reverted; same probe back under src/ returns 200
with a real VTEX orderForm. docs-only commit, no release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reload endpoint is destructive (posted body fully replaces the
in-memory registry) and its dev-bypass predicate already changed once
(import.meta.env.DEV -> NODE_ENV). Five cases: no header, no token
configured (fail-closed not fail-open), wrong token, NODE_ENV unset —
all 401 with registry untouched — plus the correct-token 200 path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocms/apps imports

The migrate/scaffold/post-cleanup pipeline still emitted (or recommended)
@decocms/start and the @decocms/apps monolith, neither of which exist for
7.x consumers -- every newly scaffolded site and every deco-post-cleanup
--fix run against an already-migrated site was writing code that doesn't
resolve. Repoints every emitted import/dependency/npx-command at the
current split packages (@decocms/blocks, @decocms/blocks-admin,
@decocms/tanstack, @decocms/blocks-cli, @decocms/apps-vtex/-shopify/
-commerce/-magento), verified against each package's actual exports map.
Also adds the missing @decocms/tanstack "./sdk/cookiePassthrough" export
(the file existed but was never exported) and broadens two post-cleanup
DETECT regexes to recognize both the old and new package names so audits
of freshly-fixed sites don't false-negative.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.5.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

tlgimenes and others added 7 commits July 8, 2026 23:45
…plies to directories

isExcludedCodegenFile() required a prefix before the marker word, so a file
literally named test.ts/spec.tsx/stories.ts (no prefix) was not excluded from
section/loader codegen scans. Anchor the marker word to either the start of
the filename or a preceding dot so bare names are caught too, while keeping
testimonials.tsx/generic.ts (marker word embedded mid-identifier) included.

Also: generate-schema.ts's findTsxFiles walk applied the exclusion check to
directory entries as well as files, so a directory literally named e.g.
foo.gen.ts would be silently skipped instead of walked. generate-sections.ts
already applied the predicate to files only; normalize generate-schema.ts to
match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ainModule()

The CLI arg-parsing block (argv → SITE_NAMESPACE/SECTIONS_REL/etc.) and the
legacy-artifact check ran unconditionally at module scope, so merely
importing this file for its pure exports (definitionIdForPath,
applyWidgetFormat, typeToJsonSchema — done by generate-schema.test.ts) ran
an fs.existsSync check against the importing process's cwd and could print
a legacy-artifact warning as an import-time side effect unrelated to what
the test wanted to exercise.

generateMeta() and the final write are already reached only inside
`if (isMainModule())`; move the arg parsing + legacy check into that same
guard (declaring the argv-derived vars with real defaults via `let` so
generateMeta() still closes over them). The CLI/subprocess behavior is
unchanged — verified via the existing generate-schema.test.ts subprocess
suite, which drives real `npx tsx generate-schema.ts [args]` invocations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
generate-sections.ts's emitted .deco/sections.gen.ts had no trailing newline
in --registry mode (the last pushed line was "};" with no separator after
it) and a doubled blank line before the registry doc comment (both the
loadingFallbacks block and the registry block unconditionally pushed a ""
separator). The same double-blank pattern also showed up between the header
comment and the SectionMetaEntry interface whenever a site has no sync/
LoadingFallback sections. Collapse any run of blank lines to a single one
and normalize to exactly one trailing newline at the final write, regardless
of which branches ran.

This changes generated bytes: every consuming site will see a one-time
regeneration diff on its next build. No action needed — sites regenerate
.deco/sections.gen.ts on every build, so the new formatting self-applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…port fix-up

Step 4 of upgradeSectionRenderer() checked `!result.includes("RenderSection")`
to decide whether to add the missing `import { RenderSection } from
"@decocms/blocks/hooks"`. Every `<RenderSection` JSX usage introduced by
steps 2/3 above already contains the substring "RenderSection", so this
check was always false and the import was never added — a nested-section
file that gained `<RenderSection>` JSX but had no matching import would
silently ship broken.

Check for the actual import statement instead of the JSX substring. Export
upgradeSectionRenderer for direct testing (mirrors the existing
writeImportedLibShims export) and add a regression fixture: a
`.Component`/`.props` call converts to `<RenderSection>` JSX with no prior
import, and the import must now be added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(a) transforms/jsx.ts and phase-cleanup.ts coordinate through the literal
"@decocms/start/hooks" import specifier: jsx.ts deliberately writes that
pre-split package path as a same-run handoff, and phase-cleanup.ts's
upgradeSectionRenderer() matches it later in the same migration run to
rewrite it to the real "@decocms/blocks/hooks" target. Neither side named
its counterpart, so a future reorder of the transform/cleanup phases could
silently desync them. Add a one-line comment on both ends.

(b) templates/package-json.ts assumes every @decocms/* package version is
lockstep (a single getLatestVersion("@decocms/blocks", ...) call covers
blocks/blocks-admin/blocks-cli/tanstack/apps-*). Document what to change if
that assumption ever breaks: each `${frameworkVersion}` usage would need
its own getLatestVersion() call.

No behavior change — comments only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…istent package

The migration's import-rewrite rules mapped "apps/shopify/hooks/$1" to
"@decocms/apps-shopify/hooks/$1", but that target has never existed: the
current @decocms/apps-shopify package ships no src/hooks/ directory and no
./hooks/* export (confirmed via package.json and `git log --diff-filter=A
-- '**/shopify/hooks/**'` returning nothing across all history, including
the pre-split @decocms/apps monolith it was migrated from wholesale in
c7604df). The legacy migration reference
(.agents/skills/deco-to-tanstack-migration/references/platform-hooks/README.md)
confirms Shopify's useCart/useUser/useWishlist were always meant to become
site-local no-op stubs, not package hooks — and
templates/hooks.ts's generateHooks() still scaffolds exactly those three at
src/hooks/use{Cart,User,Wishlist}.ts for every non-VTEX platform today.

Mirror the VTEX rule shape: route the three known hook names to the
scaffolded local files (same as "apps/vtex/hooks/useUser" → "~/hooks/useUser"
above), and drop the generic fallback entirely rather than pointing it at a
package that was never real. No shopify hook name other than these three
has ever existed, so an unmatched "apps/shopify/hooks/*" import now falls
through to the general "apps/*" catch-all (which removes the import line) —
a loud break at the usage site instead of a phantom unresolvable module.

Also adds this file's first dedicated test suite (transforms/imports.ts had
none), covering the shopify hooks fix above and splitDecoHooksImports (the
@deco/deco/hooks → useDevice/useScript split post-process), including the
mixed-import and order-of-operations cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createNextSetup's option surface (packages/nextjs/src/setup.ts) had a single
happy-path test plus a memoization/retry test. Add direct coverage for the
remaining paths: blocksDir as a real string path (tmp dir with one JSON
decofile), options.blocks winning over blocksDir on an overlapping key,
renderShell/previewWrapper/meta reaching @decocms/blocks-admin's setters
(mocked, matching the existing routeHandlers.test.ts pattern in this same
package — @decocms/blocks-admin is a heavier graph than the CMS core),
productionOrigins/customMatchers/onResolveError/onDanglingReference
passthrough to createSiteSetup, and extend() receiving the merged loaded
blocks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 7.5.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

@tlgimenes tlgimenes changed the title Split @decocms/start into a bun workspace monorepo + ship @decocms/next v7: @decocms/start → 14-package lockstep monorepo (blocks core, admin, CLI, Next.js + TanStack bindings, per-platform apps) Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant